package com.example.demo.support;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.core.io.InputStreamSource;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

@Aspect
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnProperty(value = {"log.request.enabled"},havingValue = "true",matchIfMissing = true)
public class RequestLogAspect {

    private static final Logger log = LoggerFactory.getLogger(RequestLogAspect.class);

    private static ObjectMapper objectMapper = new ObjectMapper();

    private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();

    private static final Map<Class<?>, Class<?>> primitiveWrapperTypeMap = new IdentityHashMap(8);

    static {
        // 对于空的对象转json的时候不抛出错误
        objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
        // 禁用遇到未知属性抛出异常
        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        // 序列化BigDecimal时不使用科学计数法输出
        objectMapper.configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true);
        // 日期和时间格式化
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
        objectMapper.registerModule(javaTimeModule);
    }

    @Around("(@within(org.springframework.stereotype.Controller) || @within(org.springframework.web.bind.annotation.RestController))")
    public Object aroundApi(ProceedingJoinPoint point) throws Throwable {
        HttpServletRequest request = getRequest();
        String requestUrl = ((HttpServletRequest) Objects.requireNonNull(request)).getRequestURI();
        String requestMethod = request.getMethod();
        StringBuilder beforeReqLog = new StringBuilder(300);
        List<Object> beforeReqArgs = new ArrayList();
        beforeReqLog.append("\n\n================  Request Start  ================\n");
        beforeReqLog.append("===> {}: {}");
        beforeReqArgs.add(requestMethod);
        beforeReqArgs.add(requestUrl);
        this.logIngArgs(point, beforeReqLog, beforeReqArgs);
        this.logIngHeaders(request, beforeReqLog, beforeReqArgs);
        beforeReqLog.append("================   Request End   ================\n");
        long startNs = System.nanoTime();
        log.info(beforeReqLog.toString(), beforeReqArgs.toArray());
        StringBuilder afterReqLog = new StringBuilder(200);
        List<Object> afterReqArgs = new ArrayList();
        afterReqLog.append("\n\n===============  Response Start  ================\n");
        boolean var20 = false;

        Object var13;
        try {
            var20 = true;
            Object result = point.proceed();
            afterReqLog.append("===Result===  {}\n");
            afterReqArgs.add(toJson(result));
            var13 = result;
            var20 = false;
        } finally {
            if (var20) {
                long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
                afterReqLog.append("<=== {}: {} ({} ms)\n");
                afterReqArgs.add(requestMethod);
                afterReqArgs.add(requestUrl);
                afterReqArgs.add(tookMs);
                afterReqLog.append("===============   Response End   ================\n");
                log.info(afterReqLog.toString(), afterReqArgs.toArray());
            }
        }

        long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
        afterReqLog.append("<=== {}: {} ({} ms)\n");
        afterReqArgs.add(requestMethod);
        afterReqArgs.add(requestUrl);
        afterReqArgs.add(tookMs);
        afterReqLog.append("===============   Response End   ================\n");
        log.info(afterReqLog.toString(), afterReqArgs.toArray());
        return var13;
    }

    public HttpServletRequest getRequest() {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        return requestAttributes == null ? null : ((ServletRequestAttributes)requestAttributes).getRequest();
    }

    public <T> String toJson(T obj) {
        if (obj == null) {
            return null;
        }
        try {
            return objectMapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            log.error("Parse Object to String error : {}", e.getMessage());
            return null;
        }
    }

    public MethodParameter getMethodParameter(Method method, int parameterIndex) {
        MethodParameter methodParameter = new SynthesizingMethodParameter(method, parameterIndex);
        methodParameter.initParameterNameDiscovery(PARAMETER_NAME_DISCOVERER);
        return methodParameter;
    }

    public boolean isPrimitiveOrWrapper(Class<?> clazz) {
        Assert.notNull(clazz, "Class must not be null");
        return clazz.isPrimitive() || isPrimitiveWrapper(clazz);
    }

    public boolean isPrimitiveWrapper(Class<?> clazz) {
        Assert.notNull(clazz, "Class must not be null");
        return primitiveWrapperTypeMap.containsKey(clazz);
    }

    public boolean canSerialize(@Nullable Object value) {
        return value == null ? true : objectMapper.canSerialize(value.getClass());
    }

    public String removeSuffix(CharSequence str, CharSequence suffix) {
        if (StringUtils.hasText(str) && StringUtils.hasText(suffix)) {
            String str2 = str.toString();
            return str2.endsWith(suffix.toString()) ? subPre(str2, str2.length() - suffix.length()) : str2;
        } else {
            return "";
        }
    }

    public String subPre(CharSequence string, int toIndex) {
        return sub(string, 0, toIndex);
    }

    public String sub(CharSequence str, int fromIndex, int toIndex) {
        if (!StringUtils.hasText(str)) {
            return "";
        } else {
            int len = str.length();
            if (fromIndex < 0) {
                fromIndex += len;
                if (fromIndex < 0) {
                    fromIndex = 0;
                }
            } else if (fromIndex > len) {
                fromIndex = len;
            }

            if (toIndex < 0) {
                toIndex += len;
                if (toIndex < 0) {
                    toIndex = len;
                }
            } else if (toIndex > len) {
                toIndex = len;
            }

            if (toIndex < fromIndex) {
                int tmp = fromIndex;
                fromIndex = toIndex;
                toIndex = tmp;
            }

            return fromIndex == toIndex ? "" : str.toString().substring(fromIndex, toIndex);
        }
    }

    public void logIngArgs(ProceedingJoinPoint point, StringBuilder beforeReqLog, List<Object> beforeReqArgs) {
        MethodSignature ms = (MethodSignature)point.getSignature();
        Method method = ms.getMethod();
        Object[] args = point.getArgs();
        Map<String, Object> paraMap = new HashMap(16);
        Object requestBodyValue = null;

        for(int i = 0; i < args.length; ++i) {
            MethodParameter methodParam = getMethodParameter(method, i);
            PathVariable pathVariable = (PathVariable)methodParam.getParameterAnnotation(PathVariable.class);
            if (pathVariable == null) {
                RequestBody requestBody = (RequestBody)methodParam.getParameterAnnotation(RequestBody.class);
                String parameterName = methodParam.getParameterName();
                Object value = args[i];
                if (requestBody != null) {
                    requestBodyValue = value;
                } else if (value instanceof HttpServletRequest) {
                    paraMap.putAll(((HttpServletRequest)value).getParameterMap());
                } else if (value instanceof WebRequest) {
                    paraMap.putAll(((WebRequest)value).getParameterMap());
                } else if (!(value instanceof HttpServletResponse)) {
                    String paraName;
                    if (value instanceof MultipartFile) {
                        MultipartFile multipartFile = (MultipartFile)value;
                        paraName = multipartFile.getName();
                        String fileName = multipartFile.getOriginalFilename();
                        paraMap.put(paraName, fileName);
                    } else if (value instanceof MultipartFile[]) {
                        MultipartFile[] arr = (MultipartFile[])((MultipartFile[])value);
                        if (arr.length != 0) {
                            paraName = arr[0].getName();
                            StringBuilder sb = new StringBuilder(arr.length);
                            MultipartFile[] var28 = arr;
                            int var19 = arr.length;

                            for(int var20 = 0; var20 < var19; ++var20) {
                                MultipartFile multipartFile = var28[var20];
                                sb.append(multipartFile.getOriginalFilename());
                                sb.append(",");
                            }

                            paraMap.put(paraName, removeSuffix(sb.toString(), ","));
                        }
                    } else {
                        if (value instanceof List) {
                            List<?> list = (List)value;
                            AtomicBoolean isSkip = new AtomicBoolean(false);
                            Iterator var17 = list.iterator();

                            while(var17.hasNext()) {
                                Object o = var17.next();
                                if ("StandardMultipartFile".equalsIgnoreCase(o.getClass().getSimpleName())) {
                                    isSkip.set(true);
                                    break;
                                }
                            }

                            if (isSkip.get()) {
                                paraMap.put(parameterName, "此参数不能序列化为json");
                                continue;
                            }
                        }

                        RequestParam requestParam = (RequestParam)methodParam.getParameterAnnotation(RequestParam.class);
                        paraName = parameterName;
                        if (requestParam != null && StringUtils.hasText(requestParam.value())) {
                            paraName = requestParam.value();
                        }

                        if (value == null) {
                            paraMap.put(paraName, (Object)null);
                        } else if (isPrimitiveOrWrapper(value.getClass())) {
                            paraMap.put(paraName, value);
                        } else if (value instanceof InputStream) {
                            paraMap.put(paraName, "InputStream");
                        } else if (value instanceof InputStreamSource) {
                            paraMap.put(paraName, "InputStreamSource");
                        } else if (canSerialize(value)) {
                            paraMap.put(paraName, value);
                        } else {
                            paraMap.put(paraName, "此参数不能序列化为json");
                        }
                    }
                }
            }
        }

        if (paraMap.isEmpty()) {
            beforeReqLog.append("\n");
        } else {
            beforeReqLog.append(" Parameters: {}\n");
            beforeReqArgs.add(toJson(paraMap));
        }

        if (requestBodyValue != null) {
            beforeReqLog.append("====Body=====  {}\n");
            beforeReqArgs.add(toJson(requestBodyValue));
        }

    }

    public void logIngHeaders(HttpServletRequest request, StringBuilder beforeReqLog, List<Object> beforeReqArgs) {
        Enumeration headers = request.getHeaderNames();
        while(headers.hasMoreElements()) {
            String headerName = (String)headers.nextElement();
            String headerValue = request.getHeader(headerName);
            beforeReqLog.append("===Headers===  {}: {}\n");
            beforeReqArgs.add(headerName);
            beforeReqArgs.add(headerValue);
        }
    }
}